﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using PhiStore.Util;

namespace PhiStore.Storage
{
    [Serializable()]
    internal class FtpStorage : IStorageDevice, ISerializable
    {
        private string basepath = "";
        private string username = "";
        private string password = "";
        private long reservedBytes = 0L;

        private static string directorySeparator = "/";
        private object mutex = new object();

        private Dictionary<string, FtpEntry> entries = null;
        private long usedBytes = 0L;

        private string BuildCorrectUri(string path)
        {
            return basepath + path;
        }

        /// <summary>
        /// Anonymous FTP storage
        /// </summary>
        /// <param name="serverpath"></param>
        /// <param name="bytes"></param>
        public FtpStorage(string serverpath, long bytes)
            : this(serverpath, "", "", bytes)
        { }

        public FtpStorage(string serverpath, string username, string password, long bytes)
        {
            this.basepath = serverpath;
            if (!serverpath.EndsWith(directorySeparator))
                this.basepath += directorySeparator;

            if (!serverpath.StartsWith("ftp://"))
                this.basepath = "ftp://" + this.basepath;

            this.username = username;
            this.password = password;
            this.reservedBytes = bytes;

            lock (mutex)
            {
                BuildDictionary();
            }
        }

        private DateTime GetTimestamp(string path)
        {
            DateTime timestamp = DateTime.Now;
            FtpWebResponse response = null;
            try
            {
                FtpWebRequest req = createFtpRequest(path, WebRequestMethods.Ftp.GetDateTimestamp);
                response = (FtpWebResponse)req.GetResponse();

                timestamp = response.LastModified;
            }
            catch
            {

            }
            finally
            {
                if (response != null)
                    response.Close();
            }
            return timestamp;
        }

        private void BuildDictionary()
        {

            entries = new Dictionary<string, FtpEntry>();
            Stream requestStream = null;

            try
            {
                List<string> subdirs = new List<string>();

                FtpWebRequest req = createFtpRequest(basepath, WebRequestMethods.Ftp.ListDirectoryDetails);
                requestStream = req.GetResponse().GetResponseStream();

                StreamReader lineReader = new StreamReader(requestStream);

                string line = null;
                while ((line = lineReader.ReadLine()) != null)
                {
                    string[] chunks = line.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries);

                    string filename = chunks[chunks.Length - 1];


                    if (filename == "." || filename == "..")
                    {
                        //ignore
                    }
                    else
                    {
                        string filefsize = chunks[chunks.Length - 5];
                        long filesize = long.Parse(filefsize);
                        string correctfilename = BuildCorrectUri(filename);

                        if (chunks[0].StartsWith("d"))
                        {
                            //directory
                            subdirs.Add(correctfilename + directorySeparator);

                            FtpEntry entry = new FtpEntry(0L, DateTime.Now, DateTime.Now);
                            entries.Add(correctfilename + directorySeparator, entry);
                        }
                        else
                        {

                            FtpEntry entry = new FtpEntry(filesize, GetTimestamp(correctfilename), DateTime.Now);
                            entries.Add(correctfilename, entry);
                            usedBytes += filesize;
                        }
                    }
                }

                requestStream.Close();

                foreach (string subdir in subdirs)
                {
                    req = createFtpRequest(subdir, WebRequestMethods.Ftp.ListDirectoryDetails);
                    requestStream = req.GetResponse().GetResponseStream();

                    lineReader = new StreamReader(requestStream);

                    line = null;
                    while ((line = lineReader.ReadLine()) != null)
                    {
                        string[] chunks = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

                        string filename = chunks[chunks.Length - 1];

                        long filesize = long.Parse(chunks[chunks.Length - 5]);

                        if (filename == "." || filename == "..")
                        {
                            //ignore
                        }
                        else
                        {
                            string correctfilename = subdir + filename;

                            if (chunks[0].StartsWith("d"))
                            {
                                Log.Instance.AddLogEntry(this.ToString(), "File structure broken for: " + correctfilename + " This should not be a directory");
                            }
                            else
                            {

                                FtpEntry entry = new FtpEntry(filesize, GetTimestamp(correctfilename), DateTime.Now);
                                entries.Add(correctfilename, entry);
                                usedBytes += filesize;
                            }
                        }
                    }

                }
            }
            catch
            {
                entries = null;
                Log.Instance.AddLogEntry(this.ToString(), "Failed to list objects from " + this.BasePath);
            }
            finally
            {
                if (requestStream != null)
                    requestStream.Close();
            }

        }

        private FtpWebRequest createFtpRequest(string path, string method)
        {
            FtpWebRequest req = (FtpWebRequest)FtpWebRequest.Create(new Uri(path));

            if (username != "")
            {
                req.Credentials = new NetworkCredential(username, password);
            }
            //req.KeepAlive = false;
            req.Method = method;
            req.UseBinary = true;
            return req;
        }

        private bool DirectoryExists(string path)
        {
            bool result = path == basepath || entries.ContainsKey(path);
            return result;
        }

        private bool CreateDirectory(string path)
        {
            bool result = false;
            string dirpath = path.Substring(0, path.LastIndexOf(directorySeparator) + 1);

            if (DirectoryExists(dirpath))
                result = true;
            else
            {
                try
                {
                    FtpWebRequest req = createFtpRequest(dirpath, WebRequestMethods.Ftp.MakeDirectory);
                    req.GetResponse().Close();
                    result = true;
                    entries.Add(dirpath, new FtpEntry(0L, DateTime.Now, DateTime.Now));
                }
                catch
                {
                    result = false;
                }
            }
            return result;
        }

        public bool Write(string path, byte[] bytes)
        {
            bool result = false;
            string fullpath = BuildCorrectUri(path);

            if (CreateDirectory(fullpath))
            {
                Stream requestStream = null;
                try
                {
                    FtpWebRequest req = createFtpRequest(fullpath, WebRequestMethods.Ftp.UploadFile);
                    req.ContentLength = bytes.Length;
                    req.Method = WebRequestMethods.Ftp.UploadFile;
                    requestStream = req.GetRequestStream();

                    StreamUtil.Copy(bytes, requestStream);


                    if (entries.ContainsKey(fullpath))
                    {
                        lock (mutex)
                        {
                            usedBytes += bytes.Length - entries[fullpath].Length;
                        }
                        entries[fullpath].Length = bytes.Length;
                    }
                    else
                    {
                        entries.Add(fullpath, new FtpEntry(bytes.Length, DateTime.Now, DateTime.Now));
                        lock (mutex)
                        {
                            usedBytes += bytes.Length;
                        }
                    }


                    result = true;
                }
                catch
                {
                    result = false;
                }
                finally
                {
                    if (requestStream != null)
                        requestStream.Close();
                }
            }
            return result;
        }

        public byte[] Read(string path)
        {
            byte[] result = null;
            Stream requestStream = null;
            try
            {
                FtpWebRequest req = createFtpRequest(BuildCorrectUri(path), WebRequestMethods.Ftp.DownloadFile);
                requestStream = req.GetResponse().GetResponseStream();
                result = StreamUtil.Read(requestStream);

                entries[BuildCorrectUri(path)].LastAccessTime = DateTime.Now;
            }
            catch
            {
                result = null;
            }
            finally
            {
                if (requestStream != null)
                    requestStream.Close();
            }
            return result;
        }

        public bool Delete(string path)
        {
            bool result = true;
            try
            {
                FtpWebRequest req = createFtpRequest(BuildCorrectUri(path), WebRequestMethods.Ftp.DeleteFile);
                WebResponse res = req.GetResponse();

                if (entries.ContainsKey(BuildCorrectUri(path)))
                {
                    lock (mutex)
                    {
                        usedBytes -= entries[BuildCorrectUri(path)].Length;
                    }
                    entries.Remove(BuildCorrectUri(path));
                }
            }
            catch
            {
                result = false;
            }
            finally
            {

            }
            return result;
        }

        public bool Exist(string path)
        {
            bool result = entries.ContainsKey(BuildCorrectUri(path)) ||
                entries.ContainsKey(BuildCorrectUri(path) + directorySeparator);
            return result;
        }

        public bool Rename(string oldpath, string newpath)
        {
            bool result = true;
            try
            {
                List<string> oldEntries = new List<string>();
                string cOldPath = BuildCorrectUri(oldpath);
                string cNewPath = BuildCorrectUri(newpath);

                string fromPath = cOldPath;

                if (oldpath.EndsWith(directorySeparator)) //directory
                {
                    foreach (string entry in entries.Keys)
                    {
                        if (entry.StartsWith(cOldPath))
                            oldEntries.Add(entry);
                    }
                    fromPath = BuildCorrectUri(oldpath.Remove(oldpath.Length - 1));
                }
                else //file
                {
                    oldEntries.Add(cOldPath);
                }

                FtpWebRequest request = createFtpRequest(fromPath, WebRequestMethods.Ftp.Rename);

                //it is a directory
                string toPath = newpath.EndsWith(directorySeparator) ? newpath.Remove(newpath.Length - 1) : newpath; 
                //removing directories
                toPath = toPath.Remove(0, toPath.LastIndexOf(directorySeparator) + 1);

                request.RenameTo = toPath;
               
                request.GetResponse().Close();

                foreach(string entry in oldEntries){

                    cOldPath = BuildCorrectUri(oldpath);
                    FtpEntry old = entries[entry];
                    entries.Remove(entry);
                    entries.Add(entry.Replace(cOldPath, cNewPath), old);
                }

            }
            catch
            {
                result = false;
            }

            return result;

        }

        /// <summary>
        /// Gets the paths to files
        /// </summary>
        /// <returns>the paths wihtout the basepath</returns>
        public List<string> GetPaths()
        {
            List<string> paths = null;
            try
            {
                paths = new List<string>();
                foreach (string key in entries.Keys)
                {
                    paths.Add(key.Remove(0, basepath.Length));
                }
            }
            catch
            {
                paths = null;
                Log.Instance.AddLogEntry(this.ToString(), "Failed to list objects from " + this.BasePath);
            }

            return paths;
        }

        public bool Works()
        {
            return GetPaths() != null;
        }

        public string BasePath
        {
            get { return this.basepath; }
        }

        public string DirectorySeperator
        {
            get { return directorySeparator; }
        }

        public string UserName
        {
            get { return this.username; }
        }

        public long RemainingBytes
        {
            get
            {
                long result = 0L;
                lock (mutex)
                {
                    result = this.reservedBytes - this.usedBytes;
                }
                return result;
            }
        }

        public long ReservedBytes
        {
            get { return this.reservedBytes; }
        }

        public override string ToString()
        {
            return basepath;
        }

        #region Serialization
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("basepath", basepath);
            info.AddValue("username", username);
            info.AddValue("password", password);
            info.AddValue("reservedBytes", reservedBytes);
        }

        public FtpStorage(SerializationInfo info, StreamingContext context)
        {
            this.basepath = info.GetString("basepath");
            this.username = info.GetString("username");
            this.password = info.GetString("password");
            this.reservedBytes = info.GetInt64("reservedBytes");

            lock (mutex)
            {
                BuildDictionary();
            }
        }


        #endregion

        public bool DeleteDirectory(string path)
        {

            //throw new NotImplementedException();

            string dirPath = BuildCorrectUri(path);

            List<string> allpaths = GetPaths();

            bool result = true;
            try
            {
                if (entries.ContainsKey(dirPath))
                {

                    //string[] paths = Directory.GetFiles(dirPath);

                    foreach (string p in allpaths)
                    {
                        if (p.StartsWith(path))
                        {
                            if (entries.ContainsKey(BuildCorrectUri(p)) && p != path)
                            {
                                this.Delete(p);
                            }
                        }
                    }

                    FtpWebRequest req = createFtpRequest(BuildCorrectUri(path), WebRequestMethods.Ftp.RemoveDirectory);
                    WebResponse res = req.GetResponse();
                    
                    entries.Remove(dirPath);
                }
            }
            catch
            {
                result = false;
            }
            return result;
        }

        public bool Create(string path)
        {
            bool result = false;
            if (!Exist(path))
            {
                result = CreateDirectory(BuildCorrectUri(path));
            }
            return result;
        }

        public DateTime LastAccess(string path)
        {
            DateTime result = new DateTime(1901, 1, 1); //this is oooold, as in before the kapewter was invented
            string cpath = BuildCorrectUri(path);
            foreach (KeyValuePair<string, FtpEntry> entry in entries)
            {
                if (entry.Key.StartsWith(cpath))
                {
                    if (entry.Value.LastAccessTime > result)
                        result = entry.Value.LastAccessTime;
                }
            }

            return result;
        }
    }

    class FtpEntry
    {
        public long Length;
        public DateTime LastWriteTime;
        public DateTime LastAccessTime; //must be updated manually

        public FtpEntry(long length, DateTime write, DateTime access)
        {
            this.Length = length;
            this.LastWriteTime = write;
            this.LastAccessTime = access;
        }
    }
}
